﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Medianamik.Core.Synchronization
{
    public abstract class MedianamikSynchronizer<TSource, TResult>
        where TSource : SynchronizedHierarchicalEntity, new()
        where TResult : HierarchicalEntity<TResult>
    {
        private readonly SynchronizationOptions _options;
        protected SynchronizationOptions Options { get { return _options; } }

        protected MedianamikSynchronizer()
        {
            _options = new SynchronizationOptions(); ;
        }
        protected MedianamikSynchronizer(SynchronizationOptions options)
        {
            _options = options;
        }

        #region Fields

        public List<SynchronizationNodeReport> SynchronizedNodesReport { get; private set; }

        #endregion

        #region Properties

        public IEnumerable<String> Exceptions
        {
            get { return _exceptions; }
        }
        HashSet<String> _exceptions = new HashSet<String>();

        public IEnumerable<String> Warnings
        {
            get { return _warnings; }
        }
        HashSet<String> _warnings = new HashSet<String>();

        public IEnumerable<String> Logs
        {
            get { return _logs; }
        }
        HashSet<String> _logs = new HashSet<String>();

        public HashSet<TResult> TResultBag { get { return _tResultBag; } }
        HashSet<TResult> _tResultBag = new HashSet<TResult>();

        protected IEnumerable<TSource> SourceItems
        {
            get { return _sourceItems.AsEnumerable(); }
        }
        HashSet<TSource> _sourceItems;

        #endregion

        #region Methods

        #region Public

        [Obsolete]
        public void SimulateSynchronization(IEnumerable<TSource> items)
        {
            SynchronizeTSource(items);
        }

        [Obsolete]
        public IEnumerable<TResult> Synchronize(IEnumerable<TSource> items)
        {
            return SynchronizeTSource(items);
        }

        public bool Synchronize(IEnumerable<TSource> items, out IList<TResult> results)
        {
            results = SynchronizeTSource(items);
            return Exceptions.Count() == 0;
        }

        public bool IfHasChangedThenUpdate<T>(ref T originalValue, T newValue)
        {
            var hasChanged = false;

            if (!originalValue.Equals(newValue))
            {
                originalValue = newValue;
                hasChanged = true;
            }

            return hasChanged;
        }

        #endregion

        #region Private/Protected

        //TODO: Trouver les existants (permettre d'overrider FindExisting)
        //avant de faire le ResolveTemporaryData
        //Ajouter une manière de spécifier une colonne comme identifiant (exemple : property "Name")
        private IList<TResult> SynchronizeTSource(IEnumerable<TSource> items)
        {
            SynchronizedNodesReport = new List<SynchronizationNodeReport>();

            _sourceItems = new HashSet<TSource>(items);
            var problematicItems = new HashSet<TSource>();

            foreach (var sourceItem in _sourceItems)
            {
                if (!ResolveTemporaryData(sourceItem))
                    problematicItems.Add(sourceItem);
            }

            problematicItems.ForEach(i => _sourceItems.Remove(i));

            foreach (var sourceItem in _sourceItems)
            {
                if (!(sourceItem.SynchronizeAction == SynchronizeActions.Update && !Options.ResolveLookupsOnUpdate))
                {
                    ResolveLookups(sourceItem);
                }
            }

            var orderedItems = OrderItems(_sourceItems);

            //Conversions de TSource en TResult
            var convertedItems = new List<TResult>(orderedItems
                 .Where(i => !i.IgnoreImport).Select(item =>
                 FromSourceToResult(item, FindExisting(item))).NotNull());

            convertedItems.Where(item => item.Status != EntityStates.Deleted &&
                    item.Status != EntityStates.DeletedBranch)
                .ForEach(ValidateSatelliteData);

            //Malheureusement on ne peut pas avoir la listes de erreurs
            //comme pour la validation des conversions
            //if (!simulation && Exceptions.Count() == 0)
            //{
            //    SaveAll(convertedItems);
            //}

            FinalizeSynchronization();

            return convertedItems;
        }

        protected virtual bool ResolveLookups(TSource sourceItem)
        {
            return true;
        }

        protected virtual void FinalizeSynchronization() { }

        /// <summary>
        /// Remplace SynchronizeActions.Auto
        /// par SynchronizeActions.Insert si l'item n'existe pas
        /// par SynchronizeActions.Update si l'item existe
        /// Fixe ParentId selon TemporaryParentId
        /// Fixe NodeTypeId selon NodeTypeName
        /// </summary>
        protected virtual bool ResolveTemporaryData(TSource item)
        {
            SetSynchronizeAction(item);
            return true;
        }

        protected void SetSynchronizeAction(TSource item)
        {
            if (item.SynchronizeAction == SynchronizeActions.Auto)
            {
                if (FindExisting(item) == null)
                    item.SynchronizeAction = SynchronizeActions.Insert;
                else
                    item.SynchronizeAction = SynchronizeActions.Update;
            }
        }

        protected virtual IEnumerable<TSource> OrderItems(IEnumerable<TSource> items)
        {
            //Working lists
            List<TSource> unorderedItems = new List<TSource>(items);
            List<TSource> orderedItems = new List<TSource>();

            int level = 1;

            while (unorderedItems.Count > 0)
            {
                //Tous ceux qui n'ont pas de parent ou que leur parent ne fait pas parti de cette liste
                List<TSource> itemsWithNoParent = new List<TSource>(from ui1 in unorderedItems
                                                                    where !ui1.ParentId.HasValue ||
                                                                    !(from ui2 in unorderedItems
                                                                      select ui2.Id)
                                                                               .Contains(ui1.ParentId)
                                                                    select ui1);

                foreach (var item in itemsWithNoParent)
                {
                    orderedItems.Add(item);
                    unorderedItems.Remove(item);
                }

                level++;
            }

            return orderedItems;
        }

        protected void AddException(string message)
        {
            _exceptions.Add(message);
        }

        protected void AddWarning(string message)
        {
            _warnings.Add(message);
        }

        protected void AddLog(string message)
        {
            _logs.Add(message);
        }

        protected virtual TResult FindExisting(Guid id)
        {
            return FindExisting(new TSource() { Id = id });
        }

        protected virtual TResult FindExisting(TSource source)
        {
            TResult result = TResultBag
                .Where(i => i.Status != EntityStates.Deleted)
                .FirstOrDefault(t => t.ID.Equals(source.Id));

            if (result == null)
            {
                result = GetTResult(source);

                if (result != null)
                {
                    source.Id = result.ID;
                    TResultBag.Add(result);
                }
            }

            return result;
        }

        private TResult FromSourceToResult(TSource source, TResult existingItem)
        {
            TResult result = null;

            //Validation des business rules pour que l'update/create ne plante pas
            //La validation des données satellites se fait après le traitement 
            //de conversion pour tenir compte des éléments importés
            Validate(source, existingItem);

            var nodeReport = new SynchronizationNodeReport {Success = false, Action = source.SynchronizeAction} ;
            SynchronizedNodesReport.Add(nodeReport);

            switch (source.SynchronizeAction)
            {
                case SynchronizeActions.Delete:
                    if (existingItem == null)
                        AddException(String.Format("L'item ({0}/{1}) ne peut pas être supprimé puisqu'il n'existe pas.",
                            source.Name, source.Id));
                    else
                    {
                        result = existingItem;
                        result.Status = EntityStates.Deleted;
                    }
                    break;
                case SynchronizeActions.DeleteBranch:
                    if (existingItem == null)
                        AddException(String.Format("L'item ({0}/{1}) ne peut pas être supprimé puisqu'il n'existe pas.",
                            source.Name, source.Id));
                    else
                    {
                        result = existingItem;
                        result.Status = EntityStates.DeletedBranch;
                    }
                    break;
                //break;
                case SynchronizeActions.Update:
                    // TODO: Respecter les types de propriété
                    //ResolveAndValidateSatelliteData(source, result);
                    if (existingItem != null)
                    {
                        if (UpdateTResult(source, existingItem))
                            result = existingItem;
                    }
                    else
                    {
                        AddException(String.Format("L'item ({0}/{1}) ne peut pas être mis à jour puisqu'il n'existe pas.",
                            source.Name, source.Id));
                    }
                    break;
                case SynchronizeActions.Insert:
                    if (existingItem == null)
                    {
                        result = NewTResult(source);
                        TResultBag.Add(result);
                        //TODO: Est-ce que parce qu'on insère c'est nécessairement "nouveau" ???
                        //Présentement on considère que oui (plus facile)
                    }
                    else
                        AddException(String.Format("L'item ({0}/{1}) ne peut pas être inséré puisqu'il existe déjà.",
                            source.Name, source.Id));
                    break;
                default:
                    //Auto -- Ne devrait plus y en avoir dû à la méthode SetSynchronizeActions
                    throw new InvalidOperationException();
                //break;
            }

            if (result != null)
            {
                nodeReport.Success = true;
                nodeReport.NodeId = result.ID;
            }

            return result;
            
        }

        #endregion

        #region Abstract

        //protected abstract void SaveAll(IEnumerable<TResult> items);
        protected abstract TResult GetTResult(TSource source);
        protected abstract TResult NewTResult(TSource source);
        protected abstract bool UpdateTResult(TSource source, TResult existingItem);

        #region Validation

        /// <summary>
        /// Permet de valider les business rules
        /// Exemple : La propriété Name doit commencer par une lettre majuscule et ne peut pas être null
        /// </summary>
        protected virtual void Validate(TSource source, TResult result)
        {

        }

        /// <summary>
        /// Permet de valider les références (BD) et d'éventuels modifications
        /// Exemple 1 : Le nom d'un type ne peut pas être modifié
        /// Exemple 2 : La propriété LegitimateParentRecordTypeIds d'un type fait référence à un type qui n'existe pas
        /// </summary>
        protected virtual void ValidateSatelliteData(TResult result)
        {
            //Valider le parent
            if (result.ParentID.HasValue && FindExisting(result.ParentID.Value) == null)
                AddException(String.Format("Le parent ({1}) de l'item ({0}) n'existe pas.",
                    result.ID, result.ParentID));
        }

        #endregion

        #endregion

        #endregion

    }
}
